Source: index.js

// Data related variables

/** @type {string} */
let currentDataPath;
/** @type {d3.DSVRowArray<string>} */
let currentData;
/** @type {string[]} */
let currentDataNumericAttributes;
/** @type {d3.DSVRowString<string>[]} */
let currentDataSkyline;
/** @type {d3.DSVRowString<string>[]} */
let currentDataDominated;
/** @type {d3.DSVRowString<string>[]} */
let currentDataSkylineNumericOnly;
/** @type {array[]} */
let divergingData;
// Named decisive subspaces
let decisiveSubspaces;
// The maximum number of decisive subspaces
let max_subspaces;
// Decisive subspaces in array form for Display
let decisiveSubspacesDisplay;
/**
 * Store the unique key or id field name of the data
 * @type {string}
 */
let uniqueKey;
/**
 * Store the min and max values for each numeric attribute (column).
 * @type {Object.<string, {min: number, max: number, percentage: (value: number) => number}>}
 */
let currentDataSkylineNumericOnlyMinMax;
/**
 * Store the domination score of the skyline points (`scores` property).
 * Furthermore, store the min and max domination scores.
 * @type {{min: number, max: number, scores: number[], dominatedPoints: number[][], percentage: (index: number) => number}}
 */
let currentDataSkylineDominationScores;
/**
 * Store the relative ranking of all skyline points per attribute.
 * @type {Map[]}
 */
let currentDataSkylineNumericOnlyRelativeRankings;
/** @type {number[]} */
let selectedSkylinePointIndices;

/**
 * The column index to use for tooltip titles
 * @type {number}
 */
let PointNameColumnIndex;

// HTML Elements

/** @type {d3.Selection<any, any, SVGSVGElement, any>} */
let projectionView;
/** @type {d3.Selection<any, any, HTMLElement, any>} */
let comparisonView;
/** @type {d3.Selection<any, any, HTMLElement, any>} */
let tabularView;
/** @type {d3.Selection<any, any, HTMLElement, any>} */
let attributeTable;
/** @type {d3.Selection<any, any, HTMLDivElement, any>} */
let projectionViewTooltip;
/** @type {d3.Selection<any, any, HTMLDivElement, any>}  */
let comparisonViewOverlay;
/** @type {d3.Selection<any, any, SVGSVGElement, any>}  */
let comparisonViewOverlaySvg;

// Other variables

/** @type {number[][]} */
let projectionViewPositions;
/** @type {number} */
let projectionViewSelectedIndex;

// Cell properties for the tabularView
/** @type {number} */
const cellWidth = 400;
/** @type {number} */
const cellHeight = 40;
/** @type {number} */
const detailCellHeight = 20;

/**
 * Colors for the detail matrix in the tabular view
 * @type {string[]} The hex codes
 */
const MatrixColors = [
  '#2065aa',
  '#4190c1',
  '#92c4dd',
  '#d0e4ef',
  '#f7f7f7',
  '#fcdac7',
  '#f4a481',
  '#d65f4d',
  '#b21729',
];

//
// Main file content
//

// Add all listeners that are necessary for the application to work.
document.addEventListener('DOMContentLoaded', init);
window.addEventListener('resize', () => {
  drawProjectionView(true, true);
  buildComparisonView();
});

/**
 * Perform initial configuration.
 */
async function init() {
  document.getElementById('select-available-data').selectedIndex = -1;
  currentDataPath = '';
  projectionView = d3.select('#projection-view');
  comparisonView = d3.select('#comparison-view');
  tabularView = d3.select('#tabular-view');
  attributeTable = d3.select('#attribute-table');
  projectionViewTooltip = d3.select('#projection-view-tooltip');
  comparisonViewOverlay = d3.select('#comparison-view-overlay');
  comparisonViewOverlaySvg = d3.select('#comparison-view-overlay-svg');
}

/**
 * Load new data. New data is only loaded, if it is not already loaded.
 *
 * @param {string} filepath Path to the csv-file that contains the new data.
 */
async function loadData(filepath) {
  if (currentDataPath != filepath) {
    currentDataPath = filepath;
    d3.csv(filepath).then((data) => {
      currentData = data;
      currentDataNumericAttributes = undefined; // Necessary
      // Get name of id field because it is later needed for the tabular view
      uniqueKey = d3.keys(currentData[0])[0];
      // The check `index > 0` is used because the first column is the id field
      // which shall not be considered as a numeric attribute.
      currentDataNumericAttributes = currentData.columns.filter(
        (column, index) => index > 0 && isNumericAttribute(column)
      );
      selectedSkylinePointIndices = [];
      console.log(`Successfully loaded ${data.length} records.`);
      calculateSkylinePoints();
      buildAll();
      disablePlaceholders();
    });
  }
}

/**
 * Disable all placeholders.
 */
async function disablePlaceholders() {
  d3.selectAll('.view-placeholder').style('visibility', 'hidden');
}

/**
 * Calculate the skyline points of the currently loaded data.
 */
async function calculateSkylinePoints() {
  // Calculate all skyline points.
  currentDataSkyline = currentData.filter(
    (data1) => !currentData.filter((data) => data != data1).some((data2) => dominates(data2, data1))
  );
  // Calculate all dominated points.
  currentDataDominated = currentData.filter((data1) =>
    currentData.filter((data) => data != data1).some((data2) => dominates(data2, data1))
  );

  // Use first column (0) as display name in tooltips
  // if it is a nominal column, else use second column (1)
  let firstAttr = Object.keys(currentData[0])[0];
  PointNameColumnIndex = 1;
  if (isNaN(currentData[0][firstAttr])) {
    PointNameColumnIndex = 0;
  }

  subspaces = new Map(
    currentDataNumericAttributes.map((column) => [
      column,
      currentDataSkyline.filter(
        (data1) => !currentDataSkyline.filter((data) =>
          data != data1).some((data2) => data2[column] > data1[column])
      )
    ])
  );

  // Extract the numeric attributes of all skyline points.
  currentDataSkylineNumericOnly = currentDataSkyline.map((data) =>
    currentDataNumericAttributes.reduce(
      (result, column) => (result = { ...result, [column]: +data[column] }),
      {}
    )
  );
  // Find the minimum and maximum values of all numeric attributes among all skyline points.
  currentDataSkylineNumericOnlyMinMax = currentDataNumericAttributes.reduce(
    (result, column) =>
      (result = {
        ...result,
        [column]: {
          min: Math.min(...currentDataSkyline.map((data) => data[column])),
          max: Math.max(...currentDataSkyline.map((data) => data[column])),
          percentage: function (value) {
            return (value - this.min) / (this.max - this.min);
          },
        },
      }),
    {}
  );

  // Calculate the domination scores of all skyline points.
  const dominatedPoints = currentDataSkyline.map((data1) =>
    currentData.filter((data2) => data1 != data2 && dominates(data1, data2))
  );
  const dominationScores = dominatedPoints.map((points) => points.length);
  currentDataSkylineDominationScores = {
    min: Math.min(...dominationScores),
    max: Math.max(...dominationScores),
    scores: dominationScores,
    dominatedPoints,
    percentage: function (index) {
      return (this.scores[index] - this.min) / (this.max - this.min);
    },
  };

  // Calculate the relative ranking of all attributes of all skyline points among the other skyline points.
  const relativeRankings = new Map(
    currentDataNumericAttributes.map((column) => [
      column,
      currentDataSkylineNumericOnly.map((skylinePoint, index) => ({
        index,
        value: skylinePoint[column],
      })),
    ])
  );
  relativeRankings.forEach((attributeArray) => attributeArray.sort((a, b) => a.value - b.value));
  currentDataSkylineNumericOnlyRelativeRankings = currentDataSkylineNumericOnly.map(
    (skylinePoint, index) =>
      new Map(
        Object.keys(skylinePoint).map((column) => [
          column,
          relativeRankings.get(column).findIndex((ranking) => ranking.index == index) /
            currentDataSkylineNumericOnly.length,
        ])
      )
  );

  console.log(
    `Calculated ${currentDataSkyline.length} skyline points and ${currentDataDominated.length} dominated points.`
  );

  console.log(`Determining decisive subspaces...`);

  max_subspaces = 0;
  decisiveSubspaces = new Map();
  determineDecisiveSubspaces(currentDataSkyline, currentDataNumericAttributes, currentDataNumericAttributes);
  decisiveSubspacesDisplay = new Map();
  // Bring decisive subspaces into row form for detail matrix creation
  decisiveSubspaces.forEach((val, key) => {
    if (!decisiveSubspacesDisplay.has(key))
      decisiveSubspacesDisplay.set(key, new Map());
    let subs = decisiveSubspacesDisplay.get(key);
    let rowCount = 0;
    let v = new Map([...val].sort((a, b) => {
      return (a[1].length - b[1].length);
    }));
    v.forEach(c => {
      c.forEach(n => {
        if (!subs.has(n))
          subs.set(n, []);
        subs.get(n).push(rowCount);
      });
      rowCount = rowCount + 1;
      max_subspaces = Math.max(max_subspaces, rowCount);
    });
  });

  console.log(`Determined decisive subspaces.`);

  calcDivergingData();
}

/**
 * Calculate differences between the given point and all others
 * for the current attribute (column) and remember the index of
 * the point for later highlighting. The calculation is according
 * to the papers equation in section 6.2 Tabular View
 */
async function calcDivergingData() {
  // Pre-calc the divisor for the diverging point formula in
  // the paper, 6.2 Tabular View, for every numeric column
  let divisors = new Object();
  d3.keys(currentData[0]).forEach((d) => {
    if (isNumericAttribute(d)) {
      var mean = d3.mean(currentData, (p) => +p[d]);
      var div = 0;
      currentData.forEach((dat, i) => {
        div += Math.pow(+dat[d] - mean, 2);
      });
      divisors[d] = Math.sqrt(div / currentData.length);
    }
  });

  // Interpolate between colors
  let step = d3.scaleLinear().domain([0, 8]).range([0, 8]);
  let color = d3
    .scaleLinear()
    .domain([1, step(2), step(3), step(4), step(5), step(6), step(7), step(8)])
    .range(MatrixColors.slice().reverse())
    .interpolate(d3.interpolateHcl);

  divergingData = [];
  d3.keys(currentData[0]).forEach((col, colIdx) => {
    var obj = {};
    obj[col] = [];
    divergingData.push(obj);
    if (isNumericAttribute(col)) {
      // Sort column ascending for divergence calculation
      let sortedColumn = currentData.slice().sort((a, b) => d3.ascending(+a[col], +b[col]));
      currentData.forEach((row, rowIdx) => {
        obj = {};
        obj[uniqueKey] = currentData[rowIdx][uniqueKey];
        obj['data'] = []
        divergingData[colIdx][col].push(obj);
        let mid = sortedColumn.map((e) => e[uniqueKey]).indexOf(obj[uniqueKey]);
        let point = row;
        for (let i in sortedColumn) {
          let id = sortedColumn[i][uniqueKey];
          let row = [];
          for (let n in divisors) {
            row.push((sortedColumn[i][n] - +point[n]) / divisors[n]);
          }
          obj = {};
          obj[uniqueKey] = id;
          obj['value'] = d3.sum(row, (r) => r);
          let input = i;
          let idx = 5;
          // Remap index to color range
          if (input >= mid) {
            const output_start = 5;
            const output_end = 8;
            const input_start = mid;
            let input_end = currentData.length;
            idx = output_start + ((output_end - output_start) / (input_end - input_start)) * (input - input_start)
            idx = Math.min(Math.max(Math.round(idx), output_start), output_end);
          } else if (input < mid) {
            const output_start = 0;
            const output_end = 5;
            const input_start = 0;
            let input_end = mid;
            idx =
              output_start +
              ((output_end - output_start) / (input_end - input_start)) * (input - input_start);
            idx = Math.min(Math.max(Math.round(idx), output_start), output_end);
          }
          obj['color'] = color(idx);
          divergingData[colIdx][col][rowIdx]['data'].push(obj);
        }
      });
    }
  });

  console.log(`Calculated diverging data.`);
}

/**
 * Determine decisive subspaces for every skyline point.
 * The algorithm is based on:
 * J. Pei, W. Jin, M. Ester, and Y. Tao. Catching the best views of skyline:
 * A semantic approach based on decisive subspaces. In Proceedings of the
 * 31st International Conference on Very Large Data Bases, pages 253–264.
 * VLDB Endowment, 2005
 *
 * @param skyline The current skyline of all points
 * @param subspace The subspace to search
 * @param parentSubspace The subspace of the parent
 */
function determineDecisiveSubspaces(skyline, subspace, parentSubspace) {
  // Calculate skyline for subspace
  let subspaceSkyline = currentDataSkyline.filter(
    (data1) => !currentDataSkyline.filter((data) => data != data1).some((data2) => {
      let point1 = data2; let point2 = data1;
      return (
        subspace.every((column) => +point1[column] >= +point2[column]) &&
        subspace.every((column) => +point1[column] > +point2[column])
      );
    })
  );
  if (skyline.length !== subspaceSkyline.length) {
    // Identify objects that are in parent skyline but not in current
    let x = skyline.filter(d => !subspaceSkyline.some(i => i[uniqueKey] === d[uniqueKey]));
    x.forEach(p => {
      // Create new decisive subspace list for point
      if (!decisiveSubspaces.has(p[uniqueKey]))
        decisiveSubspaces.set(p[uniqueKey], new Map());
      let subs = decisiveSubspaces.get(p[uniqueKey]);

      // Determine if subspace is superspace of already
      // existing subspaces and remove superspaces of
      // subspace
      let isSuperSpace = false;
      subs.forEach((val, key) => {
        let v = parentSubspace.every(v => val.includes(v));
        if (v)
          subs.delete(key);
        else
          isSuperSpace = val.every(v => parentSubspace.includes(v));
      })

      // Add decisive subspace if it is not a superspace
      if (!isSuperSpace)
        subs.set(parentSubspace.join(','), parentSubspace);
    });
  }
  // Search all subspaces of subspace by removing one dimension
  subspace.forEach(d => {
    determineDecisiveSubspaces(subspaceSkyline, subspace.filter(val => val !== d), subspace);
  })
}

/**
 * Build all views. Should be called when new data is loaded or
 * if the current data (or its layout) has changed
 * (e.g. when rearranging the attribute table).
 */
async function buildAll() {
  buildAttributeTable();
  buildProjectionView();
  buildComparisonView();
  buildTabularView();

  // Fix height of table (I'm not able to do it with pure CSS for some reason...)
  const tableTitleHeight = tabularView
    .node()
    .parentElement.querySelector('h2')
    .getBoundingClientRect().height;
  tabularView.classed('h-100', false);
  tabularView.style('height', `calc(100% - ${tableTitleHeight}px - 0.5em)`);
}

/**
 * Build the attribute table.
 * At first, any existing entries are removed and then
 * the table is filled with the attributes of the currently
 * loaded data.
 */
async function buildAttributeTable() {
  attributeTable.selectAll('*').remove();
  attributeTable
    .append('thead')
    .append('tr')
    .selectAll()
    .data(['Attribute Name', 'Attribute Type'])
    .enter()
    .append('th')
    .text((d) => d);
  attributeTable
    .append('tbody')
    .selectAll()
    .data(currentData.columns)
    .enter()
    .append('tr')
    .selectAll()
    .data((column) => [
      column,
      isNumericAttribute(column) ? `num: ${findMinMax(column)}` : 'nominal',
    ])
    .enter()
    .append('td')
    .text((column) => column);
}

/**
 * Build the projection view.
 *
 * DISCLAIMER: The code for the creation of the t-SNE algorithm was inspired by
 * https://bl.ocks.org/Fil/b07d09162377827f1b3e266c43de6d2a and
 * https://bl.ocks.org/Fil/33066cb4f74d35a737355f3b7a2c26b1.
 */
async function buildProjectionView() {
  const { width, height } = projectionView.node().getBoundingClientRect();
  // Calculate positions using t-SNE.
  const model = new tsnejs.tSNE();
  model.initDataDist(
    currentDataSkylineNumericOnly
      .map((data) => Object.values(data))
      .map((data1) =>
        currentDataSkylineNumericOnly
          .map((data) => Object.values(data))
          .map((data2) => d3.geoDistance(data1, data2))
      )
  );

  const dataForceSimulation = currentDataSkylineNumericOnly;
  d3.forceSimulation(dataForceSimulation.map((d) => ({ x: width / 2, y: height / 2, ...d })))
    .alpha(0.1)
    .force('tsne', (alpha) => {
      /*for (let i = 0; i < 5; i++)*/ model.step();
      projectionViewPositions = model.getSolution();
      const { centerX, centerY } = getProjectionViewProps();
      dataForceSimulation.forEach((data, index) => {
        data.x += alpha * (centerX(projectionViewPositions[index][0]) - data.x);
        data.y += alpha * (centerY(projectionViewPositions[index][1]) - data.y);
      });
    })
    .force(
      'collide',
      d3.forceCollide().radius((data) => data.r)
    )
    .on('tick', drawProjectionView)
    .on('end', () => drawProjectionView(true, true));

  model.step();
  projectionViewPositions = model.getSolution();
  drawProjectionView(false);
}

/**
 * Draw the projection view with positions given in {@link projectionViewPositions}.
 *
 * @param {boolean} update If true, the projection view gets updated. If false, the
 * projection view gets recreated.
 * @param {boolean} end If true, all listeners are added (false by default to increase performance during simulation).
 */
async function drawProjectionView(update = true, end = false) {
  if (projectionViewPositions == undefined) return;

  const { centerX, centerY } = getProjectionViewProps();
  const dominationScoreColorMin = '#fdf7ed';
  const dominationScoreColorMax = '#91191c';
  const attributeDifferenceBest = '#2662a2';
  const attributeDifferenceWorst = '#a91f2d';
  const attributeDifferenceMiddle = '#f7f8f8';
  const attributeBaseColor = '#9970ab';

  if (!update) {
    projectionView.selectAll('*').remove();
    projectionView
      .selectAll()
      .data(
        currentDataSkyline.map((data, index) => ({
          ...data,
          x: centerX(projectionViewPositions[index][0]),
          y: centerY(projectionViewPositions[index][1]),
        }))
      )
      .enter()
      .append('g')
      .attr('data-index', (d, i) => i)
      .append('circle')
      .attr('r', 4)
      .attr('cx', 0)
      .attr('cy', 0)
      .style('fill', (d, i) =>
        d3.interpolateRgb(
          dominationScoreColorMin,
          dominationScoreColorMax
        )(currentDataSkylineDominationScores.percentage(i))
      );
    projectionView
      .selectAll('g')
      .selectAll()
      .data(d3.pie()(currentDataNumericAttributes.map(() => 1)))
      .enter()
      .append('path')
      .style('fill', function (d, i) {
        if (projectionViewSelectedIndex === undefined) return attributeBaseColor;

        const column = currentDataNumericAttributes[i];
        const index = +d3.select(this.parentNode).attr('data-index');
        const value = currentDataSkylineNumericOnly[index][column];
        const minMax = currentDataSkylineNumericOnlyMinMax[column];
        const valueSelected = currentDataSkylineNumericOnly[projectionViewSelectedIndex][column];
        const valuePercentage = minMax.percentage(value);
        const valueSelectedPercentage = minMax.percentage(valueSelected);
        if (index == projectionViewSelectedIndex) return attributeBaseColor;
        if (value < valueSelected)
          return d3.interpolateRgb(
            attributeDifferenceWorst,
            attributeDifferenceMiddle
          )(valuePercentage / valueSelectedPercentage);
        else
          return d3.interpolateRgb(
            attributeDifferenceMiddle,
            attributeDifferenceBest
          )((valuePercentage - valueSelectedPercentage) / (1 - valueSelectedPercentage));
      })
      .attr(
        'd',
        d3
          .arc()
          .padAngle(0.04)
          .innerRadius(4.5)
          .outerRadius(function (d, i) {
            const column = currentDataNumericAttributes[i];
            const index = +d3.select(this.parentNode).attr('data-index');
            const value = currentDataSkylineNumericOnly[index][column];
            const minMax = currentDataSkylineNumericOnlyMinMax[column];
            return 4.5 + 12 * minMax.percentage(value);
          })
      );
  }

  projectionView.selectAll('g').attr('transform', function () {
    const index = +d3.select(this).attr('data-index');
    const x = centerX(projectionViewPositions[index][0]);
    const y = centerY(projectionViewPositions[index][1]);
    return `translate(${x},${y})`;
  });

  if (end) {
    projectionView
      .selectAll('g')
      .style('cursor', 'pointer')
      .on('mouseover', function (d) {
        const index = +d3.select(this).attr('data-index');
        const x = centerX(projectionViewPositions[index][0]);
        const y = centerY(projectionViewPositions[index][1]);
        d3.select(this).attr('transform', `translate(${x}, ${y}) scale(4)`);
        d3.select(this.parentNode)
          .selectAll('g')
          .sort((a, b) => (a == d ? 1 : -1));
        projectionViewTooltip.selectAll('*').remove();
        projectionViewTooltip.append('div').text(`${Object.keys(d)[PointNameColumnIndex]}: ${d[Object.keys(d)[PointNameColumnIndex]]}`);
        projectionViewTooltip
          .append('div')
          .text(`Domination score: ${currentDataSkylineDominationScores.scores[index]}`);
        projectionViewTooltip.style('display', 'block');

        const { width, height } = projectionViewTooltip.node().getBoundingClientRect();
        projectionViewTooltip
          .style('left', `${x - width / 2}px`)
          .style('top', `${y - height - 25}px`);

        // Scroll tabularView to matched entry
        let r = tabularView.select("[key='" + d[uniqueKey] + "']");
        r.node().scrollIntoView(true);
        // Account for fixed header and scroll back a bit
        let div = tabularView.select('.table-responsive');
        div.node().scrollBy(0, -100);
      })
      .on('mouseout', function () {
        const index = +d3.select(this).attr('data-index');
        const x = centerX(projectionViewPositions[index][0]);
        const y = centerY(projectionViewPositions[index][1]);
        d3.select(this).attr('transform', `translate(${x},${y}) scale(1)`);
        projectionViewTooltip.style('display', 'none');
      })
      .on('click', (d, i) => selectSkylinePoint(i))
      .on('dblclick', function (d, i) {
        projectionViewSelectedIndex = projectionViewSelectedIndex === undefined ? i : undefined;
        drawProjectionView(false, true);
      });
  }
}

/**
 * Calculates the properties needed to build, draw and process the projeciton view.
 *
 * @returns {{
 * centerX: d3.ScaleLinear<number, number>,
 * centerY: d3.ScaleLinear<number, number>
 * }} projection view properties
 */
function getProjectionViewProps() {
  const { width, height } = projectionView.node().getBoundingClientRect();
  const margin = 15;
  return {
    centerX: d3
      .scaleLinear()
      .range([margin, width - margin])
      .domain(d3.extent(projectionViewPositions.map((pos) => pos[0]))),
    centerY: d3
      .scaleLinear()
      .range([margin, height - margin])
      .domain(d3.extent(projectionViewPositions.map((pos) => pos[1]))),
  };
}

/**
 * Build the comparison view.
 */
async function buildComparisonView() {
  const glyphAttributes = [
    [{ x: 0.5, y: 0.5, scale: 1.0, color: '#5199cd' }],
    [
      { x: 0.5, y: 0.8, scale: 0.5, color: '#5199cd' },
      { x: 0.5, y: 0.2, scale: 0.5, color: '#f57466' },
    ],
    [
      { x: 0.5, y: 0.8, scale: 0.33, color: '#5199cd' },
      { x: 0.8, y: 0.2, scale: 0.33, color: '#f57466' },
      { x: 0.2, y: 0.2, scale: 0.33, color: '#968cd4' },
    ],
    [
      { x: 0.5, y: 0.875, scale: 0.25, color: '#5199cd' },
      { x: 0.875, y: 0.5, scale: 0.25, color: '#f57466' },
      { x: 0.5, y: 0.125, scale: 0.25, color: '#968cd4' },
      { x: 0.125, y: 0.5, scale: 0.25, color: '#f6ab55' },
    ],
  ];
  const { width, height } = comparisonView.node().getBoundingClientRect();
  const lineLength = 150;
  const textMargin = 25;
  const circleMaxRadius = 15;

  const linePosX = (index) => Math.sin((index / currentDataNumericAttributes.length) * 2 * Math.PI);
  const linePosY = (index) =>
    -Math.cos((index / currentDataNumericAttributes.length) * 2 * Math.PI);

  const xPosOf = (index) => glyphAttributes[selectedSkylinePointIndices.length - 1][index].x;
  const yPosOf = (index) => glyphAttributes[selectedSkylinePointIndices.length - 1][index].y;
  const scaleOf = (index) => glyphAttributes[selectedSkylinePointIndices.length - 1][index].scale;
  const colorOf = (index) => glyphAttributes[selectedSkylinePointIndices.length - 1][index].color;
  const scaleCorrection = (scale) => 1.0 / ((1.0 - scale) * 0.75 + scale);

  comparisonView.selectAll('*').remove();

  const comparisonViewGroupsWithData = comparisonView
    .selectAll()
    .data(selectedSkylinePointIndices)
    .enter()
    .append('g')
    .attr(
      'transform',
      (d, i) => `translate(${width * xPosOf(i)}, ${height * yPosOf(i)}) scale(${0.8 * scaleOf(i)})`
    )
    .attr('pointer-events', 'bounding-box')
    .on('mouseenter', (d, i) =>
      buildComparisonViewOverlay(
        [i],
        [currentDataSkylineDominationScores.scores[selectedSkylinePointIndices[i]]]
      )
    )
    .on('mousemove', updatePositionComparisionViewOverlay)
    .on('mouseleave', (d, i) => closeComparisonViewOverlay());
  const comparisonViewGroupsWithAttributeData = comparisonViewGroupsWithData
    .selectAll()
    .data((d, index) =>
      currentDataNumericAttributes.map((column) => ({
        column,
        indexSkylineAll: d,
        color: colorOf(index),
        scale: scaleOf(index),
      }))
    )
    .enter();
  // Radial lines for each attribute.
  comparisonViewGroupsWithAttributeData
    .append('line')
    .attr('x1', 0)
    .attr('y1', 0)
    .attr('y2', (d, i) => lineLength * linePosY(i))
    .attr('x2', (d, i) => lineLength * linePosX(i))
    .style('stroke-width', (d) => 3 * scaleCorrection(d.scale))
    .style('stroke', '#dee2e6');
  // Circle that indicates the domination score.
  comparisonViewGroupsWithData
    .append('circle')
    .attr('r', (d) => lineLength * currentDataSkylineDominationScores.percentage(d))
    .attr('cx', 0)
    .attr('cy', 0)
    .attr('stroke', (d, i) => colorOf(i))
    .attr('stroke-width', (d) => 5 * scaleCorrection(scaleOf(0)))
    .attr(
      'stroke-dasharray',
      (d) => `${5 * scaleCorrection(scaleOf(0))} ${5 * scaleCorrection(scaleOf(0))}`
    )
    .attr('fill', 'none');
  // Line that connects all attribute values.
  comparisonViewGroupsWithData
    .append('path')
    .attr('d', (d) => {
      const linePositions = currentDataNumericAttributes.map((column, i) => [
        lineLength *
          linePosX(i) *
          currentDataSkylineNumericOnlyMinMax[column].percentage(
            currentDataSkylineNumericOnly[d][column]
          ),
        lineLength *
          linePosY(i) *
          currentDataSkylineNumericOnlyMinMax[column].percentage(
            currentDataSkylineNumericOnly[d][column]
          ),
      ]);
      return d3.line()(
        linePositions.length > 0 ? [...linePositions, linePositions[0]] : linePositions
      );
    })
    .attr('stroke', (d, i) => colorOf(i))
    .attr('stroke-width', (d) => 4 * scaleCorrection(scaleOf(0)))
    .attr('fill', 'none');
  // One circle per attribute.
  // The position of the circle along the attribute axis represents
  // the the absolute attribute value (between the min and the max value of the attribute).
  // The radius of the circle represents the relative ranking of the attribute.
  comparisonViewGroupsWithAttributeData
    .append('circle')
    .attr(
      'r',
      (d) =>
        circleMaxRadius *
        currentDataSkylineNumericOnlyRelativeRankings[d.indexSkylineAll].get(d.column)
    )
    .attr(
      'cx',
      (d, i) =>
        lineLength *
        linePosX(i) *
        currentDataSkylineNumericOnlyMinMax[d.column].percentage(
          currentDataSkylineNumericOnly[d.indexSkylineAll][d.column]
        )
    )
    .attr(
      'cy',
      (d, i) =>
        lineLength *
        linePosY(i) *
        currentDataSkylineNumericOnlyMinMax[d.column].percentage(
          currentDataSkylineNumericOnly[d.indexSkylineAll][d.column]
        )
    )
    .attr('fill', (d) => d.color);
  // Labels for the attributes.
  comparisonViewGroupsWithAttributeData
    .append('text')
    .text((d) => d.column)
    .attr('x', (d, i) => (lineLength + textMargin) * linePosX(i))
    .attr('y', (d, i) => (lineLength + textMargin) * linePosY(i))
    .attr('text-anchor', 'middle')
    .attr('fill', 'black')
    .attr('font-size', (d) => 18 * scaleCorrection(d.scale));
  // Identifier (e.g. name) of the glyph.
  comparisonViewGroupsWithData
    .append('text')
    .text((d) => currentDataSkyline[d][currentData.columns[PointNameColumnIndex]])
    .attr('x', (d) => -lineLength - 75)
    .attr('y', (d) => -lineLength - 75)
    .attr('text-anchor', 'start')
    .attr('fill', (d, i) => colorOf(i))
    .attr('font-size', (d) => 36 * scaleCorrection(scaleOf(0)));

  // Domination glyphs and the lines connecting them.
  if (selectedSkylinePointIndices.length > 1) {
    const dominationGlyphBuildAttributes = [
      [{ nodes: [0, 1], offsetX: 0, offsetY: 0 }],
      [
        { nodes: [0, 1], offsetX: 0, offsetY: 0 },
        { nodes: [0, 2], offsetX: 0, offsetY: 0 },
        { nodes: [1, 2], offsetX: 0, offsetY: 0 },
        { nodes: [0, 1, 2], offsetX: 0, offsetY: 0 },
      ],
      [
        { nodes: [0, 1], offsetX: 0, offsetY: 0 },
        { nodes: [1, 2], offsetX: 0, offsetY: 0 },
        { nodes: [2, 3], offsetX: 0, offsetY: 0 },
        { nodes: [3, 0], offsetX: 0, offsetY: 0 },
        { nodes: [0, 2], offsetX: 0.1, offsetY: 0 },
        { nodes: [1, 3], offsetX: -0.1, offsetY: 0 },
        { nodes: [0, 1, 2], offsetX: 0.1, offsetY: 0 },
        { nodes: [0, 1, 3], offsetX: 0, offsetY: 0.1 },
        { nodes: [0, 2, 3], offsetX: -0.1, offsetY: 0 },
        { nodes: [1, 2, 3], offsetX: 0, offsetY: -0.1 },
        { nodes: [0, 1, 2, 3], offsetX: 0, offsetY: 0 },
      ],
    ];
    const dominationGlyphAttributes = dominationGlyphBuildAttributes[
      selectedSkylinePointIndices.length - 2
    ].map((buildAttributes) => ({
      x:
        buildAttributes.nodes.reduce((glyphPosX, nodeIndex) => glyphPosX + xPosOf(nodeIndex), 0) /
          buildAttributes.nodes.length +
        buildAttributes.offsetX,
      y:
        buildAttributes.nodes.reduce((glyphPosY, nodeIndex) => glyphPosY + yPosOf(nodeIndex), 0) /
          buildAttributes.nodes.length +
        buildAttributes.offsetY,
      colors: buildAttributes.nodes.map((nodeIndex) => colorOf(nodeIndex)),
      dominationScores: buildAttributes.nodes.map(
        (nodeIndex) =>
          currentDataSkylineDominationScores.scores[selectedSkylinePointIndices[nodeIndex]]
      ),
      exclusiveDominationScores: buildAttributes.nodes.map(
        (nodeIndex) =>
          currentDataSkylineDominationScores.dominatedPoints[
            selectedSkylinePointIndices[nodeIndex]
          ].filter(
            (dominatedPoint) =>
              !buildAttributes.nodes
                .filter((i) => i != nodeIndex)
                .some((i) =>
                  currentDataSkylineDominationScores.dominatedPoints[
                    selectedSkylinePointIndices[i]
                  ].includes(dominatedPoint)
                )
          ).length
      ),
      nodes: buildAttributes.nodes,
    }));

    // Lines connecting domination glyphs with glyphs of selected points.
    comparisonView
      .selectAll()
      .data(
        dominationGlyphAttributes.map((glyphAttributes) =>
          glyphAttributes.nodes.map((nodeIndex) => ({
            _pos1: new Victor(glyphAttributes.x, glyphAttributes.y),
            _pos2: new Victor(xPosOf(nodeIndex), yPosOf(nodeIndex)),
            dir: function () {
              return this._pos2
                .clone()
                .subtract(this._pos1)
                .multiply(new Victor(width, height))
                .normalize();
            },
            pos1: function () {
              return this._pos1
                .clone()
                .multiply(new Victor(width, height))
                .add(this.dir().multiply(new Victor(15, 15)));
            },
            pos2: function () {
              return this._pos2
                .clone()
                .multiply(new Victor(width, height))
                .subtract(
                  this.dir()
                    .multiply(new Victor(lineLength + textMargin, lineLength + textMargin))
                    .multiply(new Victor(scaleOf(0), scaleOf(0)))
                );
            },
          }))
        )
      )
      .enter()
      .append('g')
      .selectAll()
      .data((d) => d)
      .enter()
      .append('line')
      .attr('x1', (d) => d.pos1().x)
      .attr('y1', (d) => d.pos1().y)
      .attr('x2', (d) => d.pos2().x)
      .attr('y2', (d) => d.pos2().y)
      .style('stroke-width', 2)
      .style('stroke', '#dee2e6');

    // Domination glyphs.
    const dominationGlyphsWithData = comparisonView
      .selectAll()
      .data(dominationGlyphAttributes)
      .enter()
      .append('g')
      .attr('data-index', (d, i) => i)
      .attr('transform', (d) => `translate(${width * d.x}, ${height * d.y})`);
    // Inner pie chart representing domination scores.
    dominationGlyphsWithData
      .selectAll()
      .data((d) => d3.pie().sort(null)(d.dominationScores))
      .enter()
      .append('path')
      .style('fill', function (d, i) {
        const index = +d3.select(this.parentNode).attr('data-index');
        return dominationGlyphAttributes[index].colors[i];
      })
      .attr('d', d3.arc().padAngle(0.04).innerRadius(0).outerRadius(15))
      .on('mouseenter', function (d) {
        const index = +d3.select(this.parentNode).attr('data-index');
        const nodes = dominationGlyphAttributes[index].nodes;
        buildComparisonViewOverlay(
          nodes,
          nodes.map(
            (nodeIndex) =>
              currentDataSkylineDominationScores.scores[selectedSkylinePointIndices[nodeIndex]]
          )
        );
      })
      .on('mousemove', updatePositionComparisionViewOverlay)
      .on('mouseleave', closeComparisonViewOverlay);
    // Outer pie chart reprsenting exclusive domination scores.
    dominationGlyphsWithData
      .selectAll()
      .data((d) =>
        d3.pie().sort(null)(
          d.exclusiveDominationScores.reduce(
            (result, current, index) => [
              ...result,
              (d.dominationScores[index] - current) / 2,
              current,
              (d.dominationScores[index] - current) / 2,
            ],
            []
          )
        )
      )
      .enter()
      .append('path')
      .style('fill', function (d, i) {
        if (i % 3 != 1) return 'transparent';
        const glyphIndex = +d3.select(this.parentNode).attr('data-index');
        const index = (i - 1) / 3;
        return dominationGlyphAttributes[glyphIndex].colors[index];
      })
      .attr('d', d3.arc().padAngle(0.04).innerRadius(16).outerRadius(20))
      .on('mouseover', function (d) {
        const index = +d3.select(this.parentNode).attr('data-index');
        const nodes = dominationGlyphAttributes[index].nodes;
        buildComparisonViewOverlay(
          nodes,
          dominationGlyphAttributes[index].exclusiveDominationScores
        );
      })
      .on('mousemove', updatePositionComparisionViewOverlay)
      .on('mouseleave', closeComparisonViewOverlay);
  }

  function buildComparisonViewOverlay(nodeIndices, values) {
    const mousePos = d3.mouse(comparisonViewOverlay.node().parentElement);
    comparisonViewOverlaySvg.selectAll('*').remove();
    comparisonViewOverlaySvg.attr('width', '400px').attr('height', '450px');
    comparisonViewOverlay
      .style('width', '400px')
      .style('height', '450px')
      .style('left', `${mousePos[0] + 10}px`)
      .style('top', `${mousePos[1] + 10}px`);
    const overlayGroup = comparisonViewOverlaySvg
      .append('g')
      .attr('transform', `translate(200, 275) scale(0.8)`);
    const overlayGroupWithData = overlayGroup
      .selectAll()
      .data(nodeIndices.map((nodeIndex) => selectedSkylinePointIndices[nodeIndex]))
      .enter();
    const overlayGroupWithAttributeData = overlayGroupWithData
      .selectAll()
      .data((d, index) =>
        currentDataNumericAttributes.map((column) => ({
          column,
          indexSkylineAll: d,
          color: colorOf(nodeIndices[index]),
        }))
      )
      .enter();

    // The following density plot was inspired by https://www.d3-graph-gallery.com/graph/density_basic.html
    overlayGroup
      .selectAll()
      .data(
        currentDataNumericAttributes.map((column) =>
          currentDataSkylineNumericOnly.map((point) => +point[column]).sort()
        )
      )
      .enter()
      .each(function (d, i) {
        const chartHeight = d3.scaleLinear().domain([1, 20]).range([30, 10])(
          currentDataNumericAttributes.length
        );

        // Calculate the max x-value of parameter by including the first
        // two fraction-digits
        const xMin = Math.round(Math.min(...d) * 100.0) / 100.0;
        const xMax = Math.round(Math.max(...d) * 100.0) / 100.0;
        const xScale = d3.scaleLinear().domain([xMin, xMax]).nice().range([0, lineLength]);

        // The y-Axis represents the value distribution of all
        // the data which is calculated using d3's historam
        const hist = d3
          .histogram()
          .domain(xScale.domain())
          .thresholds(xScale.ticks(d.length / 2))
          .value((d) => d);

        // The max value of the y-Axis is the length of the bin
        const yMax = d3.max(hist(d), (d) => d.length);
        const yScale = d3.scaleLinear().domain([0, yMax]).nice().range([chartHeight, 0]);

        // Add a rect, containing an area chart showing the distribution
        // of the all the data
        d3.select(this)
          .append('path')
          .attr(
            'd',
            d3
              .area()
              .x((d) => xScale(+d.x0))
              .y0(chartHeight)
              .y1((d) => yScale(+d.length))(hist(d))
          )
          .attr('width', lineLength)
          .attr('height', chartHeight)
          .attr('fill', '#9a9da1')
          .attr('opacity', 0.5)
          .style(
            'transform',
            `rotate(${
              360 * (i / currentDataNumericAttributes.length) - 90
            }deg) translate(0px, -${chartHeight}px)`
          );
        d3.select(this)
          .append('path')
          .attr(
            'd',
            d3
              .area()
              .x((d) => xScale(+d.x0))
              .y0(chartHeight)
              .y1((d) => yScale(+d.length))(hist(d))
          )
          .attr('width', lineLength)
          .attr('height', chartHeight)
          .attr('fill', '#9a9da1')
          .attr('opacity', 0.5)
          .style(
            'transform',
            `rotate(${
              360 * (i / currentDataNumericAttributes.length) - 90
            }deg) scaleY(-1) translate(0px, -${chartHeight}px)`
          );
      });

    // Radial lines for each attribute.
    overlayGroup
      .selectAll()
      .data(currentDataNumericAttributes)
      .enter()
      .append('line')
      .attr('x1', 0)
      .attr('y1', 0)
      .attr('y2', (d, i) => lineLength * linePosY(i))
      .attr('x2', (d, i) => lineLength * linePosX(i))
      .style('stroke-width', 2)
      .style('stroke', '#9a9da1');
    // Line that connects all attribute values.
    overlayGroupWithData
      .append('path')
      .attr('d', (d) => {
        const linePositions = currentDataNumericAttributes.map((column, i) => [
          lineLength *
            linePosX(i) *
            currentDataSkylineNumericOnlyMinMax[column].percentage(
              currentDataSkylineNumericOnly[d][column]
            ),
          lineLength *
            linePosY(i) *
            currentDataSkylineNumericOnlyMinMax[column].percentage(
              currentDataSkylineNumericOnly[d][column]
            ),
        ]);
        return d3.line()(
          linePositions.length > 0 ? [...linePositions, linePositions[0]] : linePositions
        );
      })
      .attr('stroke', (d, i) => colorOf(nodeIndices[i]))
      .attr('stroke-width', 4)
      .attr('fill', 'none');

    if (nodeIndices.length == 1) {
      // Circle that indicates the domination score.
      overlayGroupWithData
        .append('circle')
        .attr('r', (d) => lineLength * currentDataSkylineDominationScores.percentage(d))
        .attr('cx', 0)
        .attr('cy', 0)
        .attr('stroke', (d, i) => colorOf(nodeIndices[i]))
        .attr('stroke-width', 5)
        .attr('stroke-dasharray', '5 5')
        .attr('fill', 'none');
      // One circle per attribute.
      // The position of the circle along the attribute axis represents
      // the the absolute attribute value (between the min and the max value of the attribute).
      // The radius of the circle represents the relative ranking of the attribute.
      overlayGroupWithAttributeData
        .append('circle')
        .attr(
          'r',
          (d) =>
            circleMaxRadius *
            currentDataSkylineNumericOnlyRelativeRankings[d.indexSkylineAll].get(d.column)
        )
        .attr(
          'cx',
          (d, i) =>
            lineLength *
            linePosX(i) *
            currentDataSkylineNumericOnlyMinMax[d.column].percentage(
              currentDataSkylineNumericOnly[d.indexSkylineAll][d.column]
            )
        )
        .attr(
          'cy',
          (d, i) =>
            lineLength *
            linePosY(i) *
            currentDataSkylineNumericOnlyMinMax[d.column].percentage(
              currentDataSkylineNumericOnly[d.indexSkylineAll][d.column]
            )
        )
        .attr('fill', (d) => d.color);
    }

    // Labels for the attributes.
    overlayGroup
      .selectAll()
      .data(currentDataNumericAttributes)
      .enter()
      .append('text')
      .text((d) => d)
      .attr('x', (d, i) => (lineLength + textMargin) * linePosX(i))
      .attr('y', (d, i) => (lineLength + textMargin) * linePosY(i))
      .attr('text-anchor', 'middle')
      .attr('fill', 'black')
      .attr('font-size', 18);

    const overlayTextGroup = comparisonViewOverlaySvg
      .append('g')
      .attr('transform', `translate(0, ${(4 - nodeIndices.length) * 12.5})`)
      .selectAll()
      .data(
        nodeIndices.map((nodeIndex) => ({
          nodeIndex,
          data: currentDataSkyline[selectedSkylinePointIndices[nodeIndex]],
        }))
      )
      .enter();
    overlayTextGroup
      .append('text')
      .text((d) => d.data[Object.keys(d.data)[PointNameColumnIndex]])
      .attr('x', 25)
      .attr('y', (d, i) => 25 + i * 25)
      .attr('text-anchor', 'start')
      .attr('fill', (d) => colorOf(d.nodeIndex))
      .attr('font-size', 16)
      .attr('font-weight', 'bold');
    overlayTextGroup
      .append('text')
      .text((d, i) => values[i])
      .attr('x', 400 - 25)
      .attr('y', (d, i) => 25 + i * 25)
      .attr('text-anchor', 'end')
      .attr('fill', (d) => colorOf(d.nodeIndex))
      .attr('font-size', 16)
      .attr('font-weight', 'bold');

    comparisonViewOverlay.style('display', 'block');
  }

  function updatePositionComparisionViewOverlay() {
    const mousePos = d3.mouse(comparisonViewOverlay.node().parentElement);
    comparisonViewOverlay
      .style('left', `${mousePos[0] + 10}px`)
      .style('top', `${mousePos[1] + 10}px`);
  }

  function closeComparisonViewOverlay() {
    comparisonViewOverlay.style('display', 'none');
  }

  // The following two functions were taken from https://www.d3-graph-gallery.com/graph/density_basic.html
  // Function to compute density
  function kernelDensityEstimator(kernel, X) {
    return function (V) {
      return X.map(function (x) {
        return [
          x,
          d3.mean(V, function (v) {
            return kernel(x - v);
          }),
        ];
      });
    };
  }
  function kernelEpanechnikov(k) {
    return function (v) {
      return Math.abs((v /= k)) <= 1 ? (0.75 * (1 - v * v)) / k : 0;
    };
  }
}

/**
 * Filter a key/value pair according to filter string
 * @param key The key, i.e column
 * @param value The value, i.e value in column
 * @param arg The filter argument (col = val, col < val, col > val)
 * @returns {boolean}
 */
function filterData(key, value, arg) {
  let result = false;
  let op = arg.indexOf('=') > 0 ? '=' :
    arg.indexOf('>') > 0 ? '>' :
      arg.indexOf('<') > 0 ? '<' : '';

  let parts = arg.split(op);
  parts.forEach(d => d.trim())

  let k = key.toLowerCase();
  let v = value.toLowerCase();

  switch(op) {
    case '=':
      if (parts.length < 2) return false;
      if (k === (parts[0].trim())) {
        result = (v === parts[1].trim());
      }
      break;
    case '>':
      if (parts.length < 2) return false;
      if (k === (parts[0].trim())) {
        result = (+v > +parts[1].trim());
      }
      break;
    case '<':
      if (parts.length < 2) return false;
      if (k === (parts[0].trim())) {
        result = (+v < +parts[1].trim());
      }
      break;
    default:
      result = !isNumericAttribute(key) &&
        v.indexOf(arg.trim().toLowerCase()) >= 0
  }
  return result;
}

/**
 * Build the tabular view.
 */
async function buildTabularView() {
  // Remove everything from the view
  tabularView.selectAll('*').remove();

  let controls = tabularView.append('div').attr('class', 'controls');

  // dataset represents skyline and dominated points
  let dataset = currentDataSkyline.concat(currentDataDominated);

  // Create table
  let div = tabularView.append('div').attr('class', 'table-responsive table-fixed');
  let table = div.append('table').attr('class', 'table');

  let columns = buildTabularViewTable(table, dataset);
  let rows = buildTabularViewTableBody(table, columns);

  // Initially only show skyline rows
  table.selectAll('tr.dominated-row').style('display', 'none');

  // Radiobutton to show only skyline data
  controls
    .append('input')
    .attr('type', 'radio')
    .attr('class', 'tabViewRdBtn')
    .attr('checked', 'true')
    .attr('value', 'skyline')
    .attr('name', 'toggle')
    .attr('id', 'rdskyline')
    .on('click', function () {
      table.selectAll('tr.dominated-row').style('display', 'none');
      table.selectAll('tr.skyline-row').style('display', null);
      controls.select('.tabViewLblItemCount').html(currentDataSkyline.length + ' items');
    })
    .html('Skyline Data');
  controls
    .append('label')
    .attr('for', 'rdskyline')
    .attr('class', 'tabViewLbl')
    .html('Skyline Data');

  // Radiobutton to show only dominated data
  controls
    .append('input')
    .attr('type', 'radio')
    .attr('class', 'tabViewRdBtn')
    .attr('value', 'dominated')
    .attr('name', 'toggle')
    .attr('id', 'rddominated')
    .on('click', function () {
      table.selectAll('tr.dominated-row').style('display', null);
      table.selectAll('tr.skyline-row').style('display', 'none');
      controls.select('.tabViewLblItemCount').html(currentDataDominated.length + ' items');
    });
  controls
    .append('label')
    .attr('for', 'rddominated')
    .attr('class', 'tabViewLbl')
    .html('Dominated Data');

  // Radiobutton to show both, skyline and dominated data
  controls
    .append('input')
    .attr('type', 'radio')
    .attr('class', 'tabViewRdBtn')
    .attr('value', 'all')
    .attr('name', 'toggle')
    .attr('id', 'rdall')
    .on('click', function () {
      table.selectAll('tr.dominated-row').style('display', null);
      table.selectAll('tr.skyline-row').style('display', null);
      controls.select('.tabViewLblItemCount').html(currentData.length + ' items');
    });
  controls.append('label').attr('for', 'rdall').attr('class', 'tabViewLbl').html('All Data');

  // Input textbox for highlighting matched points
  let fin = controls
    .append('input')
    .attr('type', 'text')
    .attr('class', 'tabViewTxt')
    .attr('id', 'tabTxtFilter')
    .on('keyup', (ev) => {
      if (d3.event.key === 'Enter') {
        controls.select('#tabBtnFilter').node().click();
      }
    });

  // Button to highlight points that match inputted text
  controls
    .append('input')
    .attr('type', 'button')
    .attr('class', 'tabViewBtn')
    .attr('value', 'Filter Skyline')
    .attr('id', 'tabBtnFilter')
    .on('click', function () {
      let stxt = fin.node().value;
      let entry = currentData.filter((row) => {
        return d3.entries(row).some((d) => filterData(d.key, d.value, stxt));
      });
      // Remove previous colored entries
      rows.selectAll('td').classed('highlight', false);
      // Dont color anything if all or no entries match
      if (entry.length === currentData.length || entry.length === 0) return;
      // Color nominal values of matches in red
      entry.forEach((row) => {
        let r = rows.selectAll("[key='" + row[uniqueKey] + "'] td");
        r.classed('highlight', true);
      });
      // Scroll to first matched entry
      let r = tabularView.select("[key='" + entry[0][uniqueKey] + "']");
      r.node().scrollIntoView(true);
      // Account for fixed header and scroll back a bit
      div.node().scrollBy(0, -100);
    });

  // Show number of items in table
  controls
    .append('label')
    .attr('class', 'tabViewLblItemCount')
    .html(currentDataSkyline.length + ' items');

  // Move element to front by changing the
  // order of the object. Needed to move hovered
  // lines on the distribution chart to the front
  // if they are overlapping with other points
  // From: https://gist.github.com/trtg/3922684
  d3.selection.prototype.moveToFront = function () {
    return this.each(function () {
      this.parentNode.appendChild(this);
    });
  };
}

/**
 * Build the table of the tabular view
 *
 * @param {table} table The table to build the header for
 * @returns {columns} Returns the columns of the table
 */
function buildTabularViewTable(table) {
  // Clear head
  table.selectAll('thead').selectAll('*').remove();

  // Create table header with column names
  let columns = d3.keys(currentData[0]);
  table
    .append('thead')
    .append('tr')
    .selectAll('th')
    .data(columns)
    .enter()
    .append('th')
    .text((d) => d);

  let header = new Array();
  header.columns = currentData.columns;
  header.push(currentData[0]);

  const rowsHead = table.select('thead').selectAll().data(header).enter().append('tr');

  // Add plots to show value distribution of all data as
  // the first row (includes skyline and dominated points)
  columns.forEach((col, colIdx) => {
    // Draw chart for numeric attributes
    if (isNumericAttribute(col)) {
      let chartCol = rowsHead.append('th').attr('class', 'number');
      const svg = chartCol
        .append('svg')
        .attr('class', 'area-dist-chart')
        .attr('width', cellWidth)
        .attr('height', cellHeight);

      // Calculate the max x-value of parameter by including the first
      // two fraction-digits
      const xMax = Math.round(d3.max(currentData, (n) => +n[col]) * 100.0) / 100.0;
      const xScale = d3.scaleLinear().domain([0, xMax]).nice().range([0, cellWidth]);

      // The y-Axis represents the value distribution of all
      // the data which is calculated using d3's historam
      const hist = d3
        .histogram()
        .domain(xScale.domain())
        .thresholds(xScale.ticks(currentData.length / 2))
        .value((d) => +d[col]);

      // The max value of the y-Axis is the length of the bin
      const yMax = d3.max(hist(currentData), (d) => d.length);
      const yScale = d3.scaleLinear().domain([0, yMax]).nice().range([cellHeight, 0]);

      // Add a rect, containing an area chart showing the distribution
      // of the all the data
      svg
        .selectAll('rect')
        .data([hist(currentData)])
        .enter()
        .append('path')
        .attr(
          'd',
          d3
            .area()
            .x((d) => xScale(+d.x0))
            .y0(cellHeight)
            .y1((d) => yScale(+d.length))
        )
        .attr('width', cellWidth)
        .attr('height', cellHeight);

      // Show all points as vertical lines
      svg
        .selectAll('rect')
        .data(currentData)
        .enter()
        .append('line')
        .attr('y1', 0)
        .attr('y2', cellHeight)
        .attr('class', (d) => 'skl' + '-' + d[uniqueKey])
        .attr('x1', (d) => xScale(+d[col]))
        .attr('x2', (d) => xScale(+d[col]));
    } else {
      // Empty td's for nominal columns
      rowsHead.append('th');
    }
  });
  return columns;
}

/**
 * Build the body of the tabular view table
 *
 * @param {table} table The table to build the body for
 * @param {columns} columns The columns of the table
 * @returns {columns} Returns the rows of the body
 */
function buildTabularViewTableBody(table, columns) {
  // Clear body
  table.selectAll('tbody').selectAll('*').remove();

  // Add body for data table
  let rows = table
    .append('tbody')
    .selectAll('tr')
    .data(currentData)
    .enter()
    .append('tr')
    .attr('key', (d) => d[uniqueKey])
    .attr('id', (d, i) => i + 1)
    .attr('class', d => {
      let idx = currentDataSkyline.map(e => e[uniqueKey]).indexOf(d[uniqueKey]);
      if (idx >= 0) {
        return 'skyline-row';
      } else {
        return 'dominated-row';
      }
    });

  // Tooltip to show when numeric cell is hovered
  let tabularTooltip = d3.select('#tabular-view').append('div').attr('class', 'tool-tip');

  // Fill data table with nominal values or diverging
  // bar-charts showing the difference between skyline points
  columns.forEach((col, colIdx) => {
    // Fill numeric columns with bar-charts
    if (isNumericAttribute(col)) {
      // Fill whole column with td's
      let chartCol = rows.append('td').attr('class', 'number');

      // Create a bar-chart in each td of the column
      chartCol.each(function (row, rowIdx) {
        const svg = d3
          .select(this)
          .append('svg')
          .attr('class', 'divergence-chart')
          .attr('width', cellWidth)
          .attr('height', cellHeight)
          .on('mouseover', function (d) {
            tabularTooltip.style('display', 'block');
            // Display column name and row value in tooltip
            tabularTooltip.html(col + ': ' + d[col]);

            let c = this.getBoundingClientRect();
            let p = document.getElementById('tabular-view').getBoundingClientRect();
            const tooltipwidth = tabularTooltip.node().getBoundingClientRect().width;
            // Calculate tooltip position so that it is left of the diagram
            let left = c.left - p.left - tooltipwidth;
            let top = c.top - p.top + cellHeight / 2 - 14;
            tabularTooltip.style('left', left + 'px').style('top', top + 'px');
            d3.select(this).style('background-color', '#d3d3d3');
            d3.selectAll('.skl' + '-' + d[uniqueKey])
              .classed('highlight', true)
              .moveToFront();

            // Highlight projection view glyph
            const index = currentDataSkyline.map(e => e[uniqueKey]).indexOf(d[uniqueKey]);
            if (index < 0) return;
            const node = projectionView.select("[data-index='" + index + "']");
            const { centerX, centerY } = getProjectionViewProps();
            const x = centerX(projectionViewPositions[index][0]);
            const y = centerY(projectionViewPositions[index][1]);
            node.attr('transform', `translate(${x}, ${y}) scale(4)`);
            d3.select(node.node().parentNode)
              .selectAll('g')
              .sort((a, b) => (a[uniqueKey] === d[uniqueKey] ? 1 : -1) );
            projectionViewTooltip.selectAll('*').remove();
            projectionViewTooltip.append('div').text(`${Object.keys(d)[PointNameColumnIndex]}: ${d[Object.keys(d)[PointNameColumnIndex]]}`);
            projectionViewTooltip
              .append('div')
              .text(`Domination score: ${currentDataSkylineDominationScores.scores[index]}`);
            projectionViewTooltip.style('display', 'block');

            const { width, height } = projectionViewTooltip.node().getBoundingClientRect();
            projectionViewTooltip
              .style('left', `${x - width / 2}px`)
              .style('top', `${y - height - 25}px`);
          })
          .on('mouseout', function (d) {
            tabularTooltip.style('display', 'none');
            d3.select(this).style('background-color', '#FFFFFF');
            d3.selectAll('.skl' + '-' + d[uniqueKey]).classed('highlight', false);

            // De-Highlight projection view glyph
            const index = currentDataSkyline.map(e => e[uniqueKey]).indexOf(d[uniqueKey]);
            if (index < 0) return;
            const node = projectionView.select("[data-index='" + index + "']");
            const { centerX, centerY } = getProjectionViewProps();
            const x = centerX(projectionViewPositions[index][0]);
            const y = centerY(projectionViewPositions[index][1]);
            node.attr('transform', `translate(${x},${y}) scale(1)`);
            projectionViewTooltip.style('display', 'none');
          })
          .on('click', function (d) {
            let idx = currentDataSkyline.map(e => e[uniqueKey]).indexOf(d[uniqueKey]);
            if (idx >= 0) {
              selectSkylinePoint(
                currentDataSkyline.findIndex((r) => {
                  return r[uniqueKey] === d[uniqueKey];
                })
              );
            }
          });
        let data = divergingData[colIdx][col][rowIdx]['data'];

        // Create scale for the x-axis. Scaleband splits the range into
        // n bands where n is the number of values in the range. The
        // x-axis shows how the point performs in the other dimensions
        let xScale = d3
          .scaleBand()
          .range([0, cellWidth])
          .paddingInner(0.5)
          .domain(data.map((d, i) => i));

        // The y-Axis represents the differences in all dimensions,
        // a positive value indicates positive difference
        let yMax = Math.max(
          Math.abs(d3.min(data, (d) => d['value'])),
          Math.abs(d3.max(data, (d) => d['value']))
        );
        let yScale = d3.scaleLinear().range([0, cellHeight]).domain([yMax, -yMax]);

        const dom = currentDataDominated.indexOf(row);

        // Draw baseline (bars above are positive diffs, values below negative)
        svg
          .append('g')
          .attr('class', 'baseline')
          .append('line')
          .attr('y1', yScale(0))
          .attr('y2', yScale(0))
          .attr('x1', 0)
          .attr('x2', cellWidth);

        // Draw bars showing the differences in all dimension and
        // show the bar of the current point in a different color
        // and in full y cell height
        svg
          .selectAll('rect')
          .data(
            data.filter(e =>
              currentData.find(({uniqueKey}) =>
                e[uniqueKey] === uniqueKey
              )
            )
          )
          .enter()
          .append('rect')
          .attr('x', (d, i) => xScale(i))
          .attr('y', (d, i) => {
            if (d[uniqueKey] === row[uniqueKey]) {
              return yScale(yMax);
            } else {
              if (+d['value'] < 0) {
                return cellHeight / 2;
              } else {
                return yScale(+d['value']);
              }
            }
          })
          .attr('class', (d, i) => {
            if (dom >= 0) {
              return 'divergence-chart-bar-dominated' + ' key-' + d[uniqueKey];
            }
            if (d[uniqueKey] === row[uniqueKey]) {
              return 'divergence-chart-bar-highlight' + ' key-' + d[uniqueKey];
            } else {
              return 'divergence-chart-bar' + ' key-' + d[uniqueKey];
            }
          })
          .attr('width', Math.max(xScale.bandwidth(), 1))
          .attr('yMax', function (d) {
            return Math.max(Math.abs(d3.min(d['value'])), Math.abs(d3.max(d['value'])));
          })
          .attr('height', function (d, i) {
            // Show current point with full height of cell
            if (d[uniqueKey] === row[uniqueKey]) {
              return cellHeight;
            } else {
              return cellHeight / 2 - yScale(Math.abs(+d['value']));
            }
          });

        // Small span between summary and detail matrix
        d3.select(this)
          .append('span')
          .attr('class', 'detail-matrix detail-matrix-span')
          .style('display', 'none')
          .style('width', '100%')
          .style('height', '5px');
      });
    } else {
      // Fill nominal columns with the value (id, name, etc)
      let td = rows
        .append('td')
        .attr('class', 'nominal')
        .style('cursor', 'pointer')
        .on('click', function (d) {
          let dm = tabularView.select("[key='" + d[uniqueKey] + "']").selectAll('.detail-matrix');
          if (dm.style('display') === 'none') {
            createDetailMatrix(
              currentData,
              tabularView.select("[key='" + d[uniqueKey] + "']"),
              d[uniqueKey]
            );
            dm.style('display', 'inline-flex');
          } else {
            dm.style('display', 'none');
            tabularView
              .select("[key='" + d[uniqueKey] + "']")
              .selectAll('.detail-matrix-body')
              .remove();
          }
        })
        .on('mouseover', function (d) {
          // Highlight projection view glyph
          const index = currentDataSkyline.map(e => e[uniqueKey]).indexOf(d[uniqueKey]);
          if (index < 0) return;
          const node = projectionView.select("[data-index='" + index + "']");
          const { centerX, centerY } = getProjectionViewProps();
          const x = centerX(projectionViewPositions[index][0]);
          const y = centerY(projectionViewPositions[index][1]);
          node.attr('transform', `translate(${x}, ${y}) scale(4)`);
          d3.select(node.node().parentNode)
            .selectAll('g')
            .sort((a, b) => (a[uniqueKey] === d[uniqueKey] ? 1 : -1) );
          projectionViewTooltip.selectAll('*').remove();
          projectionViewTooltip.append('div').text(`${Object.keys(d)[PointNameColumnIndex]}: ${d[Object.keys(d)[PointNameColumnIndex]]}`);
          projectionViewTooltip
            .append('div')
            .text(`Domination score: ${currentDataSkylineDominationScores.scores[index]}`);
          projectionViewTooltip.style('display', 'block');

          const { width, height } = projectionViewTooltip.node().getBoundingClientRect();
          projectionViewTooltip
            .style('left', `${x - width / 2}px`)
            .style('top', `${y - height - 25}px`);
        })
        .on('mouseout', function (d) {
          // De-Highlight projection view glyph
          const index = currentDataSkyline.map(e => e[uniqueKey]).indexOf(d[uniqueKey]);
          if (index < 0) return;
          const node = projectionView.select("[data-index='" + index + "']");
          const { centerX, centerY } = getProjectionViewProps();
          const x = centerX(projectionViewPositions[index][0]);
          const y = centerY(projectionViewPositions[index][1]);
          node.attr('transform', `translate(${x},${y}) scale(1)`);
          projectionViewTooltip.style('display', 'none');
        });

      // Centered text
      td.append('span')
        .style('display', 'inline-flex')
        .style('width', '100%')
        .style('height', cellHeight + 'px')
        .style('align-items', 'center')
        .text((row) => {
          return row[col];
        });

      // Small span between summary and detail matrix
      td.append('span')
        .attr('class', 'detail-matrix')
        .style('display', 'none')
        .style('width', '100%')
        .style('height', '5px');

      if (colIdx === 0) {
        // Add all numeric attribute names on column 0 (id, index, etc)
        d3.keys(currentData[0]).forEach((c, idx) => {
          if (isNumericAttribute(c)) {
            td.append('span')
              .attr('class', 'detail-matrix detail-matrix-header')
              .style('display', 'none')
              .style('width', '100%')
              .text(c);
          }
        });
      }

      // Mark column for decisive subspace indicators
      if (colIdx === 1) {
        td.classed('subspaces', true);
      }
    }
  });
  return rows;
}

/**
 * Creates the detail matrix of an table entry below it
 *
 * @param dataset The dataset to use
 * @param row The table row where the matrix should be appended
 * @param key The unique key of the row in the data
 */
function createDetailMatrix(dataset, row, key) {
  let tabularTooltip = d3.select('#tabular-view').append('div').attr('class', 'tool-tip');
  let colCount = 0;
  d3.keys(dataset[0]).forEach((col, colIdx) => {
    if (isNumericAttribute(col)) {
      const num = row.selectAll('td.number');
      let rowCount = colCount;
      num.each(function (r, i) {
        const detailSvg = d3
          .select(this)
          .append('svg')
          .attr('class', 'detail-matrix detail-matrix-body')
          .attr('width', cellWidth)
          .attr('height', detailCellHeight);

        let o = divergingData[colIdx][col].map((e) => e[uniqueKey]).indexOf(key);
        let data = divergingData[colIdx][col][o]['data'];

        // Create scale for the x-axis. Scaleband splits the range into
        // n bands where n is the number of values in the range. The
        // x-axis shows how the point performs in the other dimensions
        let xScale = d3
          .scaleBand()
          .range([0, cellWidth])
          .paddingInner(0.5)
          .domain(data.map((d, i) => i));

        // Sort values according to divergence bar chart
        let c = d3.keys(dataset[0])[rowCount];
        let first = divergingData[rowCount][c][o]['data'].map((e) => e[uniqueKey]);
        let sorted = data.slice().sort((a, b) => {
          return first.indexOf(a[uniqueKey]) - first.indexOf(b[uniqueKey]);
        });

        detailSvg
          .selectAll('rect')
          .data(sorted)
          .enter()
          .append('rect')
          .attr('x', (d, i) => {
            let a = xScale(i);
            return xScale(i);
          })
          .attr('y', (d, i) => 0)
          .attr('class', (d, i) => {
            if (d['value'] === 0)
              return 'detail-matrix-body-bar-highlight' + ' key-' + d[uniqueKey];
            return 'detail-matrix-body-bar' + ' key-' + d[uniqueKey];
          })
          .style('fill', (d, i) => {
            if (d['value'] !== 0) return d['color'];
          })
          .attr('width', Math.max(xScale.bandwidth(), 1))
          .attr('yMax', function (d) {
            return detailCellHeight;
          })
          .attr('height', function (d, i) {
            return detailCellHeight;
          })
          .on('mouseover', function (d) {
            tabularTooltip.style('display', 'block');
            let idx = currentData.map(e => e[uniqueKey]).indexOf(d[uniqueKey]);
            tabularTooltip.html('' + Object.values(currentData[idx])[PointNameColumnIndex]);

            let c = this.getBoundingClientRect();
            let p = document.getElementById('tabular-view').getBoundingClientRect();
            const tooltipwidth = tabularTooltip.node().getBoundingClientRect().width;
            // Calculate tooltip position so that it is on top of the bar
            let left = c.left - p.left - tooltipwidth / 2 + 2;
            let top = c.top - p.top - detailCellHeight - detailCellHeight / 2;
            tabularTooltip.style('left', left + 'px').style('top', top + 'px');

            // Highlight projection view glyph
            const index = currentDataSkyline.map(e => e[uniqueKey]).indexOf(d[uniqueKey]);
            if (index < 0) return;
            const node = projectionView.select("[data-index='" + index + "']");
            const { centerX, centerY } = getProjectionViewProps();
            const x = centerX(projectionViewPositions[index][0]);
            const y = centerY(projectionViewPositions[index][1]);
            const point = currentDataSkyline[index];
            node.attr('transform', `translate(${x}, ${y}) scale(4)`);
            d3.select(node.node().parentNode)
              .selectAll('g')
              .sort((a, b) => (a[uniqueKey] === point[uniqueKey] ? 1 : -1) );
            projectionViewTooltip.selectAll('*').remove();
            projectionViewTooltip.append('div').text(`${Object.keys(point)[PointNameColumnIndex]}: ${point[Object.keys(point)[PointNameColumnIndex]]}`);
            projectionViewTooltip
              .append('div')
              .text(`Domination score: ${currentDataSkylineDominationScores.scores[index]}`);
            projectionViewTooltip.style('display', 'block');

            const { width, height } = projectionViewTooltip.node().getBoundingClientRect();
            projectionViewTooltip
              .style('left', `${x - width / 2}px`)
              .style('top',`${y - height - 25}px`);
          })
          .on('mouseout', function (d) {
            tabularTooltip.style('display', 'none');

            // De-Highlight projection view glyph
            const index = currentDataSkyline.map(e => e[uniqueKey]).indexOf(d[uniqueKey]);
            if (index < 0) return;
            const node = projectionView.select("[data-index='" + index + "']");
            const { centerX, centerY } = getProjectionViewProps();
            const x = centerX(projectionViewPositions[index][0]);
            const y = centerY(projectionViewPositions[index][1]);
            node.attr('transform', `translate(${x},${y}) scale(1)`);
            projectionViewTooltip.style('display', 'none');
          })
          .on('click', function (d) {
            let idx = currentDataSkyline.map(e => e[uniqueKey]).indexOf(d[uniqueKey]);
            if (idx >= 0) {
              selectSkylinePoint(
                currentDataSkyline.findIndex((r) => {
                  return r[uniqueKey] === d[uniqueKey];
                })
              );
            }
          });
        rowCount = rowCount + 1;
      });
    } else {
      colCount = colCount + 1;

      let idx = currentDataSkyline.map(e => e[uniqueKey]).indexOf(key);
      // Add decisive subspace indicators
      if (colIdx === 1 && idx >= 0) {
        const nom = row.selectAll('td.nominal.subspaces');
        let subs = decisiveSubspacesDisplay.get(key);
        if (!subs) return;
        let xScale = d3
          .scaleBand()
          .range([0, 100])
          .paddingInner(0.5)
          .domain(d3.range(max_subspaces + 1));
        currentDataNumericAttributes.forEach(c => {
          const svg = nom
            .append('svg')
            .attr('class', 'detail-matrix detail-matrix-body')
            .attr('width', '100px')
            .attr('height', detailCellHeight);

          if (subs.has(c)) {
            let sub = subs.get(c);
            svg
              .selectAll('rect')
              .data(sub)
              .enter()
              .append('rect')
              .attr('x', (d, i) => {
                return xScale(d);
              })
              .attr('y', (d, i) => 0)
              .attr('class', (d, i) => {
                return 'detail-matrix-body-bar-highlight detail-matrix-body-subspace-bar-' + d;
              })
              .attr('width', Math.max(xScale.bandwidth(), 1))
              .attr('yMax', function (d) {
                return detailCellHeight;
              })
              .attr('height', function (d, i) {
                return detailCellHeight;
              })
          }
        });
      }
    }
  });
}

/**
 * Check if the column of the current data is numeric.
 *
 * @param {string} column Name of the column
 * @returns {boolean} Returns true, if the column is numeric.
 */
function isNumericAttribute(column) {
  return currentDataNumericAttributes != undefined
    ? currentDataNumericAttributes.includes(column)
    : currentData
        .map((data) => data[column])
        .every((value) => !(isNaN(+value) || isNaN(+value - 0)));
}

/**
 * Find the minimum and maximum value of the column of the current data.
 *
 * @param {string} column Name of the column
 * @returns {{min: number, max: number}} Minimum and maximum value of the column.
 */
function findMinMax(column) {
  return {
    min: Math.min(...currentDataSkyline.map((data) => parseFloat(data[column]))),
    max: Math.max(...currentDataSkyline.map((data) => parseFloat(data[column]))),
    toString() {
      return `${this.min} ~ ${this.max}`;
    },
  };
}

/**
 * Calculates the euclidean distance between two vectors.
 *
 * @param {number[]} vec1
 * @param {number[]} vec2
 * @returns {number} euclidean distance
 */
function euclideanDistance(vec1, vec2) {
  return Math.sqrt(
    vec1.reduce((previous, current, index) => (previous += current * vec2[index]), 0)
  );
}

/**
 * Checks if `point1` dominates `point2`.
 *
 * @param {d3.DSVRowString<string>} point1 Dominating point
 * @param {d3.DSVRowString<string>} point2 Dominated point
 * @return {boolean} true, if `point1` dominates `point2`.
 */
function dominates(point1, point2) {
  return (
    currentDataNumericAttributes.every((column) => +point1[column] >= +point2[column]) &&
    currentDataNumericAttributes.some((column) => +point1[column] > +point2[column])
  );
}

/**
 * Selects or deselects a skyline point. A maximum of 4 points can be selected.
 *
 * @param {d3.DSVRowString<string>} skylinePointIndex Index of the skyline point the shall be de-/selected
 */
function selectSkylinePoint(skylinePointIndex) {
  const index = selectedSkylinePointIndices.indexOf(skylinePointIndex);
  if (index > -1) {
    selectedSkylinePointIndices.splice(index, 1);
  } else {
    if (selectedSkylinePointIndices.length >= 4) return;
    selectedSkylinePointIndices.push(skylinePointIndex);
  }
  buildComparisonView();
}